/**
 *	---------------------------------------------------------------------------
 *	COMP 6231/FALL 2013
 *	Assignment - 03
 *	@author Talha Rahman (1296884)
 *	---------------------------------------------------------------------------
 *	This is the main server class. This class provides all the required services (add,edit,count,transfer records)
 *	and also creates threads to provide UDP services or send UDP requests to 
 *  other servers.
 *	  
 */
package ca.concordia.comp6231.server;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.FileHandler;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.logging.SimpleFormatter;

import ca.concordia.comp6231.record.CriminalRecord;
import ca.concordia.comp6231.record.MissingRecord;
import ca.concordia.comp6231.record.Record;
import ca.concordia.comp6231.record.RecordStore;
import ca.concordia.comp6231.udp.UDPClientThread;
import ca.concordia.comp6231.udp.UDPService;

public class StationServer implements StationServerInterface{
	private String serverName;
	private ServerConfiguration config;
	private Thread udpService;
	private UDPClientThread udpClientThread;
	
	private ConcurrentHashMap<String, Integer> currentID;
	
	public final RecordStore recordStore;
	
	private final static Logger logger = Logger.getLogger(StationServer.class.getName());
	
	/**
	 * Constructor
	 * @param serverName The name of the server (i.e. SPL,SPB,SPVM).
	 */
	public StationServer(String serverName){
		try {
			config = new ServerConfiguration(serverName);
		} catch (Exception e) {
			logger.log(Level.SEVERE, "Couldn't load configuration. Invalid server name: " + serverName);
			System.exit(-1);
		}
		
		this.setServerName(serverName);
		recordStore = new RecordStore();
		
		String logFileName = "Server_" + getServerName() + "_" + System.currentTimeMillis() + ".log";
		initLogging(logFileName);
		
		loadCurrentID();		
		loadUDPService();
	}
	
	public StationServer(){
		this("SPL");
	}
	
	/**
	 * Loads the data structure for storing the last used Record ID.
	 */
	private void loadCurrentID(){
		currentID = new ConcurrentHashMap<String, Integer>();
		currentID.put("MR", 9999);
		currentID.put("CR", 9999);
	}
	
	/**
	 * Loads the UDP service.
	 */
	private void loadUDPService(){
		int udpPortNo = config.getUDPPortNo();
		udpService = new Thread(new UDPService(udpPortNo, this));
		
		try{
			udpService.start();
			String message = " Server: " + getServerName() + " started running UDP service at port: " +	udpPortNo + ".";
			logger.log(Level.INFO, message);
			System.out.println("[" + new Date(System.currentTimeMillis()) + "]" + message);
		}catch(Exception e){
			String message = " Server: " + getServerName() + " failed to start UDP service at port: " + udpPortNo + ".";
			logger.log(Level.SEVERE, message);
			System.out.println("[" + new Date(System.currentTimeMillis()) + "]" + message);
		}		
	}
	
	/**
	 * Sends a UDP message by creating an UDP client thread. 
	 * @see {@link UDPClientThread}
	 * @param message The UDP message to send.
	 * @param hostName The UDP host to send the message to.
	 * @param portNo The UDP port no.
	 * @return An instance of the UDP Client Thread.
	 */
	public UDPClientThread sendUDPMessage(String message, String hostName, int portNo){
		udpClientThread = new UDPClientThread(message, hostName, portNo);
		udpClientThread.start();
		
		return udpClientThread;
	}
	
	/**
	 * Creates a Criminal Record.
	 * @return The server response for this attempted action.
	 * @throws Exception 
	 */
	@Override
	public String createCRecord(String fName, String lName, String crimeDesc, String status, String badgeID) throws Exception {
		
		if(!isStatusSettable("CR", status)){
			throw new Exception("Invalid status for this type of record.");
		}
		
		String ID = generateID("CR");
		CriminalRecord record = new CriminalRecord(ID, fName, lName, crimeDesc, status);
		String firstLetterOfLastName = lName.substring(0, 1).toUpperCase();
		
		logger.log(Level.INFO, "Attempting to add new Criminal record : " + record + " invoked by badge ID: " + badgeID);
		
		recordStore.addNewRecord(firstLetterOfLastName.charAt(0), record);
		
		String response = "[" + new Date(System.currentTimeMillis()) + "]" + " Server: " + getServerName()   
							+ " Added record:" + record + " invoked by badge ID: " + badgeID;
		logger.log(Level.INFO, response);
		
		return response;
	}
	
	/**
	 * Creates a Missing Record.
	 * @return The server response for this attempted action.
	 */
	@Override
	public String createMRecord(String fName, String lName, String description, String address, String lastDate,
							String lastLocation, String status, String badgeID) throws Exception{
		
		if(!isStatusSettable("MR", status)){
			throw new Exception("Invalid status for this type of record.");
		}
		
		String ID = generateID("MR");
		MissingRecord record = new MissingRecord(ID, fName, lName, description, address, lastDate, lastLocation, status);
		String firstLetterOfLastName = lName.substring(0, 1).toUpperCase();
		
		logger.log(Level.INFO, "Attempting to add new Missing record : " + record + " invoked by badge ID: " + badgeID);
		
		recordStore.addNewRecord(firstLetterOfLastName.charAt(0), record);
		
		String response = "[" + new Date(System.currentTimeMillis()) + "]" + " Server: " + getServerName()   
				+ " Added record:" + record + " invoked by badge ID: " + badgeID;
		logger.log(Level.INFO, response);

		return response;
	}
	
	/**
	 * Gets the the record count for all three servers.
	 * @return The server response for this attempted action.
	 */
	@Override
	public String getRecordCounts(String badgeID) throws Exception{
		StringBuilder response = new StringBuilder(); 
		response.append(this.getServerName() + " " + getRecordCountForCurrentServer());
		
		List<String> serverNames = config.getAvailableServerNames();
		
		logger.log(Level.INFO, "Attempting to getRecordCounts." + " invoked by badge ID: " + badgeID);
		
		List<UDPClientThread> udpRequestList = new ArrayList<UDPClientThread>(); 
		for(String server:serverNames){
			if(!server.equals(this.getServerName())){
				UDPClientThread udpRequest = sendUDPMessage("getRecordCount", 
														config.getHostName(server), 
														config.getUDPPortNo(server));
				udpRequestList.add(udpRequest);
			}
		}
		
	
		for (UDPClientThread udpRequest : udpRequestList) {
			try {
				udpRequest.join();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			
			String udpResponse = udpRequest.getUDPResponseString().trim();
			if (!udpResponse.equals("Invalid Request.")) {
				response.append("," + udpResponse);
			}
		}
		
		String responseString = new String(response);
		logger.log(Level.INFO, responseString);
		
		return responseString;
	}
	
	/**
	 * Gets the record count for the current instance of the RMI server.
	 * @return The server response for this attempted action.
	 */
	public int getRecordCountForCurrentServer(){
		return recordStore.getRecordCount();
	}
	
	/**
	 * Edits a record
	 * @return The server response for this attempted action.
	 */
	@Override
	public String editRecord(String lName, String recordID, String newStatus,  String badgeID) throws Exception{		
		Record record = recordStore.findRecordByLastNameAndID(lName, recordID);
		if(record == null){
			throw new Exception("Record not found.");
		}
		if(!isStatusSettable(record.getRecordType(), newStatus)){
			throw new Exception("Invalid status for this type of record." );
		}
		
		logger.log(Level.INFO, "Attempting to editRecord " + " invoked by badge ID: " + badgeID);
		
		recordStore.editRecord(recordID, lName, newStatus);
		
		String response = "[" + new Date(System.currentTimeMillis()) + "]" + " Server: " + getServerName()   
				+ " Edited record:" + record + " invoked by badge ID: " + badgeID;

		logger.log(Level.INFO, response);
		
		return response;
	}
	
	@Override
	public String transferRecord(String recordID, String remoteStationServerName, String badgeID) {
		
		Record record = recordStore.findRecordByID(recordID);

		if (record == null) {
			return "Record not found with ID : " + recordID;
		}

		StringBuilder response = new StringBuilder();
		List<String> serverNames = config.getAvailableServerNames();
		List<UDPClientThread> udpRequestList = new ArrayList<UDPClientThread>();
		String responseString = "";

		if (!serverNames.contains(remoteStationServerName)) {
			return "Server not found  : " + remoteStationServerName;
		}
		
		logger.log(Level.INFO, "Attempting to transferRecord " + " invoked by badge ID: " + badgeID);

		synchronized (record) {
			UDPClientThread udpRequest = sendUDPMessage("transferRecord="
					+ record, config.getHostName(remoteStationServerName),
					config.getUDPPortNo(remoteStationServerName));
			udpRequestList.add(udpRequest);

			for (UDPClientThread udpReq : udpRequestList) {
				try {
					udpReq.join();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}

				String udpResponse = udpReq.getUDPResponseString().trim();
				if (!udpResponse.equals("Invalid Request.")) {
					response.append(udpResponse);
				}
			}

			responseString = new String(response);
			logger.log(Level.INFO, "Result of Tranfer Record attempt :"
					+ responseString + " invoked by badgeID :" + badgeID);
			if (responseString.startsWith("Transferred record")) {
				recordStore.deleteRecordById(recordID);
			}
		}

		return responseString;
	}

	
	/**
	 * Checks if a status (Missing, Found, InProgress, etc.) is valid for a certain type of record.
	 * @param recordType The type of the record.
	 * @param status The status.
	 * @return Returns true if the status is valid for this type of record - false otherwise.
	 */
	private boolean isStatusSettable(String recordType, String status){
		return config.isValidStatus(recordType, status);
	}
	
	/**
	 * Prints all the records to the screen (for the current server).
	 */
	@Override
	public void printAllRecords(){		
		recordStore.printAllRecords();
	}

	/**
	 * Getter for the server name.
	 * @return The server name.
	 */
	public String getServerName() {
		return serverName;
	}

	/**
	 * Setter for the server name.
	 * @param serverName The new server name.
	 */
	public void setServerName(String serverName) {
		this.serverName = serverName;
	}
	
	/**
	 * Generates and ID for the type of the record.
	 * @param type The type of record (MR for Missing Record, CR for Criminal Record)
	 * @return The generated ID.
	 */
	public String generateID(String type){
		int lastID = currentID.get(type);
		currentID.put(type, ++lastID);
		
		return type + lastID;
	}
	
	/**
	 * Initialize the logging objects for the server application
	 * @param fileName The log file name.
	 */
	private static void initLogging(String fileName){
		FileHandler fh = null;
		try {
			fh = new FileHandler(fileName, false);
		} catch (Exception e) {
			e.printStackTrace();
		}
		
		fh.setFormatter(new SimpleFormatter()); 
		logger.setUseParentHandlers(false);
		logger.addHandler(fh);
		
		logger.setLevel(Level.INFO);
	}
}
